<?php
// XMLFile -- Initial version June 1, 2000 
// May 29, 2002
//  Fixed to work with later versions of PHP that have deprecated the call-time
//  pass by reference syntax.  This may break versions of PHP older than 3, so
//  if you are using this on those versions, go with an earlier version.
//  Also added some more comments on how to use it to create XML files.  Some
//  people (a surprising number of them, in fact) were having difficulty
//  understanding the recursive nature of XML files, and that each tag has
//  subtags, each of which can have subtags, each of which....
// July 12, 2001
//  Fixed an oops in the find_subtags_by_name function.
// July 11, 2001
//  Incorporated Kevin Howe's read_xml_string function.
//  Incorporated Mike Konkov's find_subtags_by_name function (with some changes).
//  Fixed an errant documentation comment (open instead of fopen -- oops).
//
// September 29, 2000
// by Chris Monson -- e408345b17be3ce90059d01d96be0599@orangatango.com This PHP module is licensed under the GNU LGPL (www.gnu.org)
// Please become familiar with this license before sending this thing all over
// the place!  I would like to have any changes submitted back to me, and this
// comment must be included with any distribution of this component.
//
// The purpose of this module was to provide a simple interface for dealing with
// small to medium sized XML files.  It is probably not suitable for very large
// files since it reads the entire file into a structure in memory.  For very
// large files, the XML parsing functions should be used more or less directly
// so that pieces of the file can be dealt with as they are read in.
//
// The basic idea is this: Read the XML file into an internal tree structure
// that is easy to traverse.  This module also allows you to create such a
// structure in memory and then write it out to disk.  The output is formatted
// in a nice, readable way, using whitespace to delimit tag containment, etc.
// It makes it very easy to write nice-looking XML files.
//
// I have included some usage comments.  They are almost certainly incomplete.
// If you get stumped, first use the source, then email me.
//
//------------------------------------------------------------------------------
//## USAGE ###
//------------------------------------------------------------------------------
// Reading an XML file:
//------------------------------------------------------------------------------
//
// $xml = new XMLFile();
// $fh = fopen( 'myxmlfile.xml', 'r' );
// $xml->read_file_handle( $fh );
// close( $fh );
//
// Now the tags can be accessed via the root node of $xml:
//
// $root = &$xml->roottag;
// $tagname = $root->name;
// $tagdata = $root->cdata;
//
// Note that a tag is of the following form:
// <NAME attribute=value>CDATA</NAME>
// Each tag contains an attributes array, which is associative by nature.  In
// other words, you would access the value of "attribute" as follows:
//
// $value = $root->attributes['attribute'];
//
// Also, each tag has a 'tags' proprerty, which is an ordered array (integer
// indices, not associative!) which has the tags that were contained within
// this tag in their order of appearance.  The reason that this is not
// associative is that there can be multiple tags of the same name.  There is
// nothing in the XML spec (barring a DTD) that declares the uniqueness of tag
// names.  For example:
//
// <OUTER>
//     <INNER>CDATA</INNER>
//     <INNER name="hello"/>
// </OUTER>
//
// In the above example, the outer tag would have a tags array that has two
// entries, each of which has a tag name of "INNER".  The one with CDATA wrapped
// inside would be at index 0, and the other would be at index 1.
//
// Once you have finished with the XMLFile object, you need to call the cleanup
// method.  If you don't, you will get a memory leak, since PHP is reference
// counted and each element in the tree refers to its parent.  'cleanup' simply
// traverses the tree and disconnects the parent pointers.  The PHP cleans up
// everything else.
//
// $xml->cleanup();
// 
// Note that you can change the elements, delete tags, and do other things
// to the tree once it has been read from the file.  After it has been changed,
// you can write it back out and the file will reflect your changes.
//------------------------------------------------------------------------------
// Writing a new file:
// 
// $xml = new XMLFile();
// $xml->create_root(); # necessary -- no root is created until requested
// $xml->roottag->name = 'ROOT';
// $xml->roottag->add_subtag( 'INNER', array() );
// $innertag = &$xml->roottag->curtag;
// $innertag->add_subtag( 'REALLYINNER', array() );
// # Or, you can do this:
// $xml->roottag->curtag->add_subtag( 'INNER2', array() );
// # The point is that each tag can have subtags.  The most recently added
// # subtag is always the curtag of its parent.
// $xml->roottag->add_subtag( 'INNER', array( 'name' => 'value' ) );
// $xml->roottag->curtag->cdata = "Hello!"; # curtag is the most recent addition
// $fh = fopen( 'myxmlfile.xml', 'w' );
// $xml->write_file_handle( $fh );
// close( $fh );
//
// The file will look like this: (no space between ? and >)
//
// < ?xml version="1.0" encoding="UTF-8" ? >
// <ROOT>
//     <INNER>
//           <REALLYINNER/>
//           <INNER2/>
//     </INNER>
//     <INNER name="value">Hello!</INNER>
// </ROOT>
//
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------


if (! isset ( $XMLFile_Included ) || ! $XMLFile_Included) {
	$XMLFile_Included = 1;
	
	###############################################################################
	class XMLTag {
		var $cdata;
		var $attributes;
		var $name;
		var $tags;
		var $parent;
		
		var $curtag;
		
		function XMLTag(&$parent) {
			if (is_object ( $parent )) {
				$this->parent = &$parent;
			}
			$this->_init ();
		}
		
		function _init() {
			$this->attributes = array ();
			$this->cdata = '';
			$this->name = '';
			$this->tags = array ();
		}
		
		function add_subtag($name, $attributes = 0) {
			$tag = new XMLTag ( $this );
			$tag->set_name ( $name );
			if (is_array ( $attributes )) {
				$tag->set_attributes ( $attributes );
			}
			$this->tags [] = &$tag;
			$this->curtag = &$tag;
		}
		
		function find_subtags_by_name($name) {
			$result = array ();
			$found = false;
			for($i = 0; $i < $this->num_subtags (); $i ++) {
				if (strtoupper ( $this->tags [$i]->name ) == strtoupper ( $name )) {
					$found = true;
					$array2return [] = &$this->tags [$i];
				}
			}
			if ($found) {
				return $array2return;
			} else {
				return false;
			}
		}
		
		function clear_subtags() {
			# Traverse the structure, removing the parent pointers
			$numtags = sizeof ( $this->tags );
			$keys = array_keys ( $this->tags );
			foreach ( $keys as $k ) {
				$this->tags [$k]->clear_subtags ();
				unset ( $this->tags [$k]->parent );
			}
			
			# Clear the tags array
			$this->tags = array ();
			unset ( $this->curtag );
		}
		
		function remove_subtag($index) {
			if (is_object ( $this->tags [$index] )) {
				unset ( $this->tags [$index]->parent );
				unset ( $this->tags [$index] );
			}
		}
		
		function num_subtags() {
			return sizeof ( $this->tags );
		}
		
		function add_attribute($name, $val) {
			$this->attributes [strtoupper ( $name )] = $val;
		}
		
		function clear_attributes() {
			$this->attributes = array ();
		}
		
		function set_name($name) {
			$this->name = strtoupper ( $name );
		}
		
		function set_attributes($attributes) {
			$this->attributes = (is_array ( $attributes )) ? $attributes : array ();
		}
		
		function add_cdata($data) {
			$this->cdata .= $data;
		}
		
		function clear_cdata() {
			$this->cdata = "";
		}
		
		function write_file_handle($fh, $prepend_str = '') {
			# Get the attribute string
			$attrs = array ();
			$attr_str = '';
			foreach ( $this->attributes as $key => $val ) {
				$attrs [] = strtoupper ( $key ) . "=\"$val\"";
			}
			if ($attrs) {
				$attr_str = join ( " ", $attrs );
			}
			# Write out the start element
			$tagstr = "$prepend_str<{$this->name}";
			if ($attr_str) {
				$tagstr .= " $attr_str";
			}
			
			$keys = array_keys ( $this->tags );
			$numtags = sizeof ( $keys );
			# If there are subtags and no data (only whitespace), 
			# then go ahead and add a carriage
			# return.  Otherwise the tag should be of this form:
			# <tag>val</tag>
			# If there are no subtags and no data, then the tag should be
			# closed: <tag attrib="val"/>
			$trimmeddata = trim ( $this->cdata );
			if ($numtags && ($trimmeddata == "")) {
				$tagstr .= ">\n";
			} elseif (! $numtags && ($trimmeddata == "")) {
				$tagstr .= "/>\n";
			} else {
				$tagstr .= ">";
			}
			
			fwrite ( $fh, $tagstr );
			
			# Write out the data if it is not purely whitespace
			if ($trimmeddata != "") {
				fwrite ( $fh, $trimmeddata );
			}
			
			# Write out each subtag
			foreach ( $keys as $k ) {
				$this->tags [$k]->write_file_handle ( $fh, "$prepend_str\t" );
			}
			
			# Write out the end element if necessary
			if ($numtags || ($trimmeddata != "")) {
				$tagstr = "</{$this->name}>\n";
				if ($numtags) {
					$tagstr = "$prepend_str$tagstr";
				}
				fwrite ( $fh, $tagstr );
			}
		}
	
	}
	###############################################################################
	class XMLFile {
		var $parser;
		var $roottag;
		var $curtag;
		
		function XMLFile() {
			$this->init ();
		}
		
		# Until there is a suitable destructor mechanism, this needs to be
		# called when the file is no longer needed.  This calls the clear_subtags
		# method of the root node, which eliminates all circular references
		# in the xml tree.
		function cleanup() {
			if (is_object ( $this->roottag )) {
				$this->roottag->clear_subtags ();
			}
		}
		
		function init() {
			$this->roottag = "";
			$this->curtag = &$this->roottag;
		}
		
		function create_root() {
			$null = 0;
			$this->roottag = new XMLTag ( $null );
			$this->curtag = &$this->roottag;
		}
		
		# read_xml_string
		# Same as read_file_handle, but you pass it a string.  Note that
		# depending on the size of the XML, this could be rather memory intensive.
		# Contributed July 06, 2001 by Kevin Howe
		function read_xml_string($str) {
			$this->init ();
			$this->parser = xml_parser_create ( "UTF-8" );
			xml_set_object ( $this->parser, $this );
			xml_set_element_handler ( $this->parser, "_tag_open", "_tag_close" );
			xml_set_character_data_handler ( $this->parser, "_cdata" );
			xml_parse ( $this->parser, $str );
			xml_parser_free ( $this->parser );
		}
		
		function read_file_handle($fh) {
			$this->init ();
			$this->parser = xml_parser_create ( "UTF-8" );
			xml_set_object ( $this->parser, $this );
			xml_set_element_handler ( $this->parser, "_tag_open", "_tag_close" );
			xml_set_character_data_handler ( $this->parser, "_cdata" );
			
			while ( $data = fread ( $fh, 4096 ) ) {
				if (! xml_parse ( $this->parser, $data, feof ( $fh ) )) {
					die ( sprintf ( "XML error: %s at line %d", xml_error_string ( xml_get_error_code ( $this->parser ) ), xml_get_current_line_number ( $this->parser ) ) );
				}
			}
			
			xml_parser_free ( $this->parser );
		}
		
		function write_file_handle($fh, $write_header = 1) {
			if ($write_header) {
				fwrite ( $fh, "<?xml version='1.0' encoding='UTF-8'?>\n" );
			}
			
			# Start at the root and write out all of the tags
			$this->roottag->write_file_handle ( $fh );
		}
		
		###### UTIL #######
		

		function _tag_open($parser, $tag, $attributes) {
			#print "tag_open: $parser, $tag, $attributes\n";
			# If the current tag is not set, then we are at the root
			if (! is_object ( $this->curtag )) {
				$null = 0;
				$this->curtag = new XMLTag ( $null );
				$this->curtag->set_name ( $tag );
				$this->curtag->set_attributes ( $attributes );
			} else { # otherwise, add it to the tag list and move curtag
				$this->curtag->add_subtag ( $tag, $attributes );
				$this->curtag = &$this->curtag->curtag;
			}
		}
		
		function _tag_close($parser, $tag) {
			# Move the current pointer up a level
			$this->curtag = &$this->curtag->parent;
		}
		
		function _cdata($parser, $data) {
			$this->curtag->add_cdata ( $data );
		}
	}
	###############################################################################
} // included
###############################################################################
